// Copyright (c) Appeon. All rights reserved.
// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. 
// You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0.

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using System.Transactions;

namespace SnapObjects.Data
{
    internal abstract class ModelChangeTracker<TModel> : IModelChangeTracker<TModel>
    {
        protected readonly IDictionary<TModel, StatefulModel<TModel>> _modelsTracked;
        protected readonly IModelFactory<TModel> _modelFactory;
        protected IModelValidator<TModel> _validator;
        protected ChangeTrackingStrategy _changeTrackingStrategy;

        public ModelChangeTracker(ChangeTrackingStrategy changeTrackingStrategy)
            : this(ModelFactoryFactory.Create<TModel>(), changeTrackingStrategy)
        {
        }

        // For DynamicModel, Non-strong type
        public ModelChangeTracker(
            IModelFactory<TModel> modelFactory,
            ChangeTrackingStrategy changeTrackingStrategy)
            : this(modelFactory, modelFactory.GetValidator(), changeTrackingStrategy)
        {
        }

        protected ModelChangeTracker(
            IModelFactory<TModel> modelFactory,
            IModelValidator<TModel> dataValidator,
            ChangeTrackingStrategy changeTrackingStrategy)
        {
            _modelsTracked = new Dictionary<TModel, StatefulModel<TModel>>();
            _changeTrackingStrategy = changeTrackingStrategy;
            _modelFactory = modelFactory;
            _validator = dataValidator;
        }

        protected virtual UpdateSingularExecutor UpdateSingularExecutor
            => UpdateSingularExecutor.Instance;

        protected virtual UpdateBatchExecutor UpdateBatchExecutor
            => UpdateBatchExecutor.Instance;

        public void CancelTrack(TModel model)
        {
            _modelsTracked.Remove(model);
        }

        public void Reset(ChangeTrackingStrategy changeTrackingStrategy)
        {
            _modelsTracked.Clear();
            _changeTrackingStrategy = changeTrackingStrategy;
        }

        public UpdateResult Update(
            DataContext dataContext,
            UpdateSqlStrategy updateSqlStrategy,
            UpdateWhereStrategy updateWhereStrategy,
            bool resetState = true)
        {
            UpdateResult result = null;

            var updatableModels = this.GetUpdateableModels(updateSqlStrategy, updateWhereStrategy);

            var hasError = false;
            var batchExecute = dataContext.ContextOptions.BatchExecuting;
            IAdoDbTransaction transaction = null;

            if (dataContext.CurrentTransaction == null && Transaction.Current == null)
            {
                transaction = dataContext.BeginTransaction();
            }

            try
            {
                if (batchExecute)
                {
                    result = this.UpdateBatchExecutor.Update(dataContext, updatableModels);
                }
                else
                {
                    result = this.UpdateSingularExecutor.Update(dataContext, updatableModels);
                }
            }
            catch
            {
                hasError = true;
                throw;
            }
            finally
            {
                if (transaction != null)
                {
                    if (hasError || result.Cancelled)
                    {
                        dataContext.Rollback();
                    }
                    else
                    {
                        dataContext.Commit();
                    }
                }
            }

            if (resetState)
            {
                this.ResetState(StateTrackable.AllTracked);
            }

            return result;
        }

        public Task<UpdateResult> UpdateAsync(
            DataContext dataContext,
            UpdateSqlStrategy updateSqlStrategy,
            UpdateWhereStrategy updateWhereStrategy,
            bool resetState = true)
        {
            return this.UpdateAsync(dataContext, updateSqlStrategy, updateWhereStrategy, resetState, default);
        }
        
        public async Task<UpdateResult> UpdateAsync(
            DataContext dataContext,
            UpdateSqlStrategy updateSqlStrategy,
            UpdateWhereStrategy updateWhereStrategy,
            bool resetState,
            CancellationToken cancellationToken = default)
        {
            UpdateResult result = null;

            var updatableModels = this.GetUpdateableModels(updateSqlStrategy, updateWhereStrategy);

            var hasError = false;
            var batchExecute = dataContext.ContextOptions.BatchExecuting;
            IAdoDbTransaction transaction = null;

            if (dataContext.CurrentTransaction == null && Transaction.Current == null)
            {
                transaction = await dataContext.BeginTransactionAsync(cancellationToken);
            }

            try
            {
                if (batchExecute)
                {
                    result = await this.UpdateBatchExecutor.UpdateAsync(
                        dataContext, updatableModels, cancellationToken);
                }
                else
                {
                    result = await this.UpdateSingularExecutor.UpdateAsync(
                        dataContext, updatableModels, cancellationToken);
                }
            }
            catch
            {
                hasError = true;
                throw;
            }
            finally
            {
                if (transaction != null)
                {
                    if (hasError || result.Cancelled)
                    {
                        dataContext.Rollback();
                    }
                    else
                    {
                        dataContext.Commit();
                    }
                }
            }

            if (resetState)
            {
                this.ResetState(StateTrackable.AllTracked);
            }

            return result;
        }

        protected virtual List<IUpdatableModel> GetUpdateableModels(
            UpdateSqlStrategy updateSqlStrategy,
            UpdateWhereStrategy updateWhereStrategy)
        {
            var deleteModels = new List<IUpdatableModel>();
            var modifiedModels = new List<IUpdatableModel>();
            var newModels = new List<IUpdatableModel>();

            var modelMeta = _modelFactory.GetAccessor().ModelMeta;

            foreach (var modelToUpdate in _modelsTracked)
            {
                //Models checked out
                /*if (modelToUpdate.Value.ModelState == ModelState.NotModified &&
                    modelToUpdate.Value.CheckedOut &&
                    modelToUpdate.Value.AnyValueChanged())
                {
                    modelToUpdate.Value.ModelState = ModelState.Modified;
                }*/

                var modelState = modelToUpdate.Value.LatestModelState;

                if (modelState == ModelState.NewModified ||
                    modelState == ModelState.Deleted ||
                    modelState == ModelState.Modified)
                {
                    //valid model
                    if (modelState != ModelState.Deleted)
                    {
                        var isValid = _validator.IsValid(modelToUpdate.Key,
                                                out var validationResults);
                        if (!isValid)
                        {
                            throw new ValidationException(validationResults.FirstOrDefault().ErrorMessage);
                        }
                    }

                    IUpdatableModel updatableModel = null;
                    if (updateSqlStrategy == UpdateSqlStrategy.DeleteThenInsert &&
                        modelState == ModelState.Modified &&
                        modelToUpdate.Value.HasModifiedKey())
                    {
                        if (modelToUpdate.Value is IUpdateSplitable updateSplitable)
                        {
                            updatableModel = new UpdatableModel(
                                modelMeta,
                                _changeTrackingStrategy,
                                updateWhereStrategy,
                                updateSplitable.SplitDeletedFromUpdate(_changeTrackingStrategy),
                                true);

                            deleteModels.Add(updatableModel);

                            updatableModel = new UpdatableModel(
                                modelMeta,
                                _changeTrackingStrategy,
                                updateWhereStrategy,
                                updateSplitable.SplitInsertedFromUpdate(_changeTrackingStrategy),
                                true);

                            newModels.Add(updatableModel);
                        }
                        else
                        {
                            throw new InvalidOperationException("Only POCO statefully model supports DeleteThenInsert.");
                        }
                    }
                    else
                    {
                        updatableModel = new UpdatableModel(
                            modelMeta,
                            _changeTrackingStrategy,
                            updateWhereStrategy,
                            modelToUpdate.Value,
                            true);

                        if (modelState == ModelState.Deleted)
                        {
                            deleteModels.Add(updatableModel);
                        }
                        else if (modelState == ModelState.Modified)
                        {
                            modifiedModels.Add(updatableModel);
                        }
                        else
                        {
                            newModels.Add(updatableModel);
                        }
                    }
                }
            }

            var updatableModels = new List<IUpdatableModel>();

            updatableModels.AddRange(deleteModels);
            updatableModels.AddRange(modifiedModels);
            updatableModels.AddRange(newModels);

            return updatableModels;
        }

        public int Count(StateTrackable stateTrackable)
        {
            if (stateTrackable == StateTrackable.AllTracked)
            {
                return _modelsTracked.Count;
            }
            else if (stateTrackable == StateTrackable.NewAndNewModified)
            {
                return this.GetCount(this.ToModelState(StateTrackable.New)) +
                            this.GetCount(this.ToModelState(StateTrackable.NewModified));
            }
            else
            {
                return this.GetCount(this.ToModelState(stateTrackable));
            }
        }

        public void ResetState(StateTrackable stateTrackable)
        {
            if (stateTrackable == StateTrackable.AllTracked)
            {
                _modelsTracked.Clear();
            }
            else if (stateTrackable == StateTrackable.NewAndNewModified)
            {
                this.RemoveStateInfo(this.ToModelState(StateTrackable.New));
                this.RemoveStateInfo(this.ToModelState(StateTrackable.NewModified));
            }
            else
            {
                this.RemoveStateInfo(this.ToModelState(stateTrackable));
            }
        }

        protected int GetCount(ModelState modelState)
        {
            var count = 0;

            foreach (var pair in _modelsTracked)
            {
                if (pair.Value.LatestModelState == modelState)
                {
                    count++;
                }
            }

            return count;
        }

        private void RemoveStateInfo(ModelState modelState)
        {
            IList<TModel> models = new List<TModel>();

            foreach (var pair in _modelsTracked)
            {
                if (pair.Value.ModelState == modelState)
                {
                    models.Add(pair.Key);
                }
            }

            foreach (var model in models)
            {
                _modelsTracked.Remove(model);
            }
        }

        protected ModelState ToModelState(StateTrackable stateReset)
        {
            if (stateReset == StateTrackable.Modified)
            {
                return ModelState.Modified;
            }
            else if (stateReset == StateTrackable.New)
            {
                return ModelState.New;
            }
            else if (stateReset == StateTrackable.NewModified)
            {
                return ModelState.NewModified;
            }
            else if (stateReset == StateTrackable.Deleted)
            {
                return ModelState.Deleted;
            }
            else
            {
                return ModelState.NotTracked;
            }
        }

        public bool IsTracked(TModel model)
        {
            return _modelsTracked.ContainsKey(model);
        }

        public virtual bool TrackDeleted(IList<TModel> models)
        {
            foreach (var model in models)
            {
                this.TrackDeleted(model);
            }

            return true;
        }

        public virtual bool TrackDeleted(TModel model)
        {
            var statefulModel = this.FindTrackedStatefulModel(model);

            if (statefulModel != null) //already tracked
            {
                if (statefulModel.ModelState == ModelState.New ||
                    statefulModel.ModelState == ModelState.NewModified)
                {
                    _modelsTracked.Remove(model);
                }
                else if (statefulModel.ModelState == ModelState.Modified ||
                    statefulModel.ModelState == ModelState.NotModified ||
                    statefulModel.CheckedOut)
                {
                    statefulModel.ChangeModelStateToDelete();
                }
                else
                {
                    throw new InvalidOperationException("Invalid model state.");
                }
            }
            else
            {
                _modelsTracked.Add(model, this.CreateStatefulModel(model, ModelState.Deleted));
            }

            return true;
        }

        public virtual bool TrackNew(TModel model, bool modified = false)
        {
            if (this.IsTracked(model))
            {
                throw new InvalidOperationException("The model is already tracked");
            }

            var statefulModel = this.CreateStatefulModel(
                model, modified ? ModelState.NewModified : ModelState.New);

            statefulModel.CheckedOut = true;

            _modelsTracked.Add(model, statefulModel);

            if (_modelsTracked.TryGetValue(model, out var tempStatefulModel))
            {
                tempStatefulModel.ApplyDefaultValue(model);
            }

            return true;
        }

        public TModel Track(IModelEntry<TModel> modelEntry)
        {
            var statefulModel = EntryStatefulModelFactory.Create(
                _modelFactory, modelEntry, _changeTrackingStrategy);

            return this.InternalTrack(modelEntry, statefulModel);
        }

        protected TModel InternalTrack(IModelEntry<TModel> modelEntry, IStatefulModel statefulModel)
        {
            var model = (TModel)statefulModel.Current;

            if (_changeTrackingStrategy == ChangeTrackingStrategy.PropertyState)
            {
                var accessor = _modelFactory.GetAccessor();

                foreach (var propertyName in modelEntry.Properties)
                {
                    var index = accessor.IndexOf(propertyName);

                    var propertyState = modelEntry.GetPropertyState(propertyName);

                    if (index >= 0 && propertyState == PropertyState.Modified)
                    {
                        statefulModel.SetPropertyState(index, propertyState);
                    }
                }
            }

            _modelsTracked.Add(model, (StatefulModel<TModel>)statefulModel);

            return model;
        }

        public void TrackCheckOut(TModel model)
        {
            var statefulModel = this.FindTrackedStatefulModel(model);

            if (statefulModel == null)
            {
                statefulModel = this.CreateStatefulModel(model, ModelState.NotModified);
                _modelsTracked.Add(model, statefulModel);
            }

            statefulModel.CheckedOut = true;
        }

        private StatefulModel<TModel> TrackModified(TModel model)
        {
            if (this.IsTracked(model))
            {
                throw new InvalidOperationException("The model is already tracked");
            }

            var statefulModel = this.CreateStatefulModel(model, ModelState.Modified);
            _modelsTracked.Add(model, statefulModel);

            return statefulModel;
        }

        public void MarkPropertyModified(TModel model, int propertyIndex)
        {
            var statefulModel = this.MarkModelModified(model);
            statefulModel.SetPropertyState(propertyIndex, PropertyState.Modified);
        }

        public void MarkAllPropertiesModified(TModel model)
        {
            var statefulModel = this.MarkModelModified(model);
            statefulModel.ResetPropertyState(PropertyState.Modified);
        }

        public void MarkAllPropertiesNotModified(TModel model)
        {
            var statefulModel = this.FindTrackedStatefulModel(model);

            if (statefulModel != null)
            {
                statefulModel.ResetPropertyState(PropertyState.NotModified);
            }
        }

        private StatefulModel<TModel> MarkModelModified(TModel model)
        {
            var statefulModel = this.FindTrackedStatefulModel(model);

            if (statefulModel == null)
            {
                statefulModel = this.TrackModified(model);
            }
            else if (statefulModel.ModelState == ModelState.New)
            {
                statefulModel.ModelState = ModelState.NewModified;
            }
            else if (statefulModel.ModelState == ModelState.NotModified)
            {
                statefulModel.ModelState = ModelState.Modified;
            }
            else if (statefulModel.ModelState == ModelState.NotTracked)
            {
                statefulModel.ModelState = ModelState.Modified;
            }

            return statefulModel;
        }

        public ModelState GetModelState(TModel model)
        {
            var statefulModel = this.FindTrackedStatefulModel(model);

            return statefulModel == null ?
                ModelState.NotTracked : statefulModel.LatestModelState;
        }

        public bool TryGetOriginalValue(TModel model, int propertyIndex, out object value)
        {
            var statefulModel = this.FindTrackedStatefulModel(model);

            if (statefulModel == null)
            {
                value = null;
                return false;
            }
            else
            {
                value = statefulModel.GetOriginalValue(propertyIndex);
                return true;
            }
        }

        public bool TryGetOriginalValue(TModel model, string propertyName, out object value)
        {
            var statefulModel = this.FindTrackedStatefulModel(model);

            if (statefulModel == null)
            {
                value = null;
                return false;
            }
            else
            {
                value = statefulModel.GetOriginalValue(propertyName);
                return true;
            }
        }

        public bool TryGetOriginalValue<TValue>(TModel model, Func<TModel, TValue> selector, out TValue value)
        {
            var statefulModel = this.FindTrackedStatefulModel(model);

            if (statefulModel == null)
            {
                value = default;
                return false;
            }
            else
            {
                value = (TValue)statefulModel.GetOriginalValue(m => selector((TModel)m));

                return true;
            }
        }

        public void SetModelState(TModel model, ModelState modelState)
        {
            var statefulModel = this.FindTrackedStatefulModel(model);

            if (statefulModel == null && modelState != ModelState.NotTracked)
            {
                statefulModel = this.TrackModified(model);
            }

            if (modelState == ModelState.NotTracked)
            {
                this.CancelTrack(model);
            }

            statefulModel.ModelState = modelState;
        }

        public PropertyState GetPropertyState(TModel model, int propertyIndex)
        {
            var statefulModel = this.FindTrackedStatefulModel(model);

            return statefulModel == null ? PropertyState.NotModified :
                                                statefulModel.GetPropertyState(propertyIndex);
        }

        public PropertyState GetPropertyState(TModel model, int propertyIndex, bool ignoreDefaultValue)
        {
            if (!ignoreDefaultValue)
            {
                var property = _modelFactory.GetAccessor().ModelMeta.Properties
                               .Where(m => m.Index == propertyIndex).FirstOrDefault();

                if (property != null && property is IPocoProperty p)
                {
                    if (this.GetModelState(model) == ModelState.NewModified && p.Attribute.DefaultValueDefined)
                    {
                        return PropertyState.Modified;
                    }
                }
            }

            return this.GetPropertyState(model, propertyIndex);
        }

        public void SetPropertyState(TModel model, int index, PropertyState propertyState)
        {
            var statefulModel = this.FindTrackedStatefulModel(model);

            if (statefulModel != null)
            {
                statefulModel.SetPropertyState(index, propertyState);

                if (propertyState == PropertyState.Modified
                    && (statefulModel.ModelState == ModelState.New || statefulModel.ModelState == ModelState.NotTracked))
                {
                    this.MarkPropertyModified(model, index);
                }

                if (propertyState == PropertyState.NotModified && statefulModel.ModelState != ModelState.NewModified)
                {
                    this.SyncModelStateFromProperties(model);
                }
            }
            else
            {
                //throw new InvalidOperationException("The model is not tracked for change.");
                if (propertyState == PropertyState.Modified)
                {
                    this.MarkPropertyModified(model, index);
                }
            }
        }

        public void SyncModelStateFromProperties(TModel model)
        {
            var statefulModel = this.FindTrackedStatefulModel(model);

            if (statefulModel != null)
            {
                if (!statefulModel.HasModifiedProperty())
                {
                    this.SetModelState(model, ModelState.NotModified);
                }
            }
        }

        public void ResetPropertyState(TModel model, PropertyState propertyState)
        {
            var statefulModel = this.FindTrackedStatefulModel(model);

            if (statefulModel != null)
            {
                statefulModel.ResetPropertyState(propertyState);
            }
            else
            {
                throw new InvalidOperationException("The model is not tracked for change.");
            }
        }

        protected abstract StatefulModel<TModel> CreateStatefulModel(TModel model, ModelState modelState);

        protected StatefulModel<TModel> FindTrackedStatefulModel(TModel model)
        {
            if (_modelsTracked.TryGetValue(model, out var statefulModel))
            {
                return statefulModel;
            }
            else
            {
                return null;
            }
        }

        public IList<ValidationResult> Validate(TModel model)
        {
            _validator.IsValid(model, out var validationResults);

            return validationResults;
        }

        public IList<ValidationResult> ValidateAll()
        {
            var results = new List<ValidationResult>();

            foreach (var modelToUpdate in _modelsTracked)
            {
                _validator.IsValid(modelToUpdate.Key, out var validationResults);

                if (validationResults.Count > 0)
                {
                    results.AddRange(validationResults);
                }
            }

            return results;
        }
    }
}
